
/*
 * Copyright (C) 2008-2009 Archie L. Cobbs. All rights reserved.
 *
 * $Id: ReferenceField.java 377 2010-03-11 20:50:18Z archie.cobbs $
 */

package org.dellroad.sidekar;

import java.util.regex.Matcher;

import org.dellroad.sidekar.util.SidekarException;
import org.dellroad.stuff.string.ParseContext;

/**
 * A {@link Field} that contains a reference to an {@link Item} that is
 * an instance of a specific {@link Entity} or any sub-{@link Entity}.
 *
 * <p>
 * Reference fields may be {@code null}, which is their default value.
 * </p>
 *
 * @param <T> Java type used to represent the referenced item
 * @see org.dellroad.sidekar.annotation.SidekarField
 */
public final class ReferenceField<T> extends SimpleField<T> {

    private final Entity<T> referent;

    /**
     * Constructor.
     *
     * @param name field name
     * @param entity entity containing this field, or null
     * @param referent the entity an instance of to which this reference will refer
     */
    ReferenceField(String name, Entity<?> entity, Entity<T> referent) {
        super(name, entity, referent.getUserClass(), referent.getModelClass());
        this.referent = referent;
    }

    /**
     * Get the entity to which this field refers.
     */
    public Entity<T> getReferent() {
        return this.referent;
    }

    /**
     * Cast the object to the this field's Java type.
     *
     * @throws ClassCastException if the object is not an instance of this field's referent entity
     * @throws IllegalStateException if the database containing this field's referent entity has been closed
     */
    @Override
    public T cast(Object obj) {
        T item = super.cast(obj);
        if (item != null && !this.referent.isInstance(item))
            throw new ClassCastException("not an instance of " + this.referent);
        return item;
    }

    @Override
    public boolean equals(T value1, T value2) {
        return value1 == value2;
    }

    @Override
    public void visit(FieldSwitch fieldSwitch) {
        fieldSwitch.caseReferenceField(this);
    }

    @Override
    public boolean canReference(Entity<?> target) {
        return this.referent.isAncestorOf(target);
    }

    /**
     * @throws DetachedItemException {@inheritDoc}
     * @throws IllegalArgumentException {@inheritDoc}
     * @throws IllegalStateException {@inheritDoc}
     * @throws SidekarException {@inheritDoc}
     */
    @Override
    public void set(Item<?> item, T newReferent) {
        this.cast(newReferent);
        super.set(item, newReferent);
    }

    /**
     * Encode the reference as a string. This returns a string of the form {@code null} if the
     * reference is null, otherwise {@code Name@1234} where {@code Name} is the name of the
     * referent's {@link Entity} and {@code 1234} is the referent's {@linkplain IdField id}.
     */
    @Override
    public String toString(T value) {
        Item<?> item = (Item<?>)cast(value);
        return item != null ? item.getSidekarEntity().getName() + "@" + item.getSidekarId() : "null";
    }

    /**
     * @throws IllegalArgumentException {@inheritDoc}
     * @throws IllegalStateException {@inheritDoc}
     */
    @Override
    public T fromString(ParseContext ctx) {
        this.validate();
        if (ctx.tryLiteral("null"))
            return null;
        Matcher matcher = ctx.matchPrefix("(\\p{javaJavaIdentifierStart}[\\p{javaJavaIdentifierPart}]*)@([0-9]+)");
        Entity<?> entity = this.referent.getSchema().getEntity(matcher.group(1));
        if (!this.referent.isAncestorOf(entity))
            throw new IllegalArgumentException(entity + " is not assignable to " + this.referent);
        return this.cast(this.referent.get(Long.parseLong(matcher.group(2))));
    }
}

